﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace GerbilLib
{
    /// <summary>
    /// Provides a Singleton TaskManager object so all background tasks that are
    /// being executed can be tracked and active feedback on status given to the
    /// user.
    /// </summary>
    public class TaskManager
    {
        /// <summary>
        /// Event Handler for New Task Added Event.
        /// </summary>
        public static event EventHandler<TaskEventArgs> TaskAddedEvent;
        /// <summary>
        /// Event Handler for Finished Task Removed Event.
        /// </summary>
        public static event EventHandler<TaskEventArgs> TaskRemovedEvent;

        /// <summary>
        /// Singleton TaskManager Object Instance
        /// </summary>
        private static readonly TaskManager instance = new TaskManager();
        /// <summary>
        /// Static list of currently active task objects
        /// </summary>
        public static Dictionary<Task, string> CurrentTasks = new Dictionary<Task, string>();

        /// <summary>
        /// Private constructor needed for Singleton pattern
        /// </summary>
        private TaskManager()
        {
            // Do nothing
        }

        /// <summary>
        /// Provides access to Singleton TaskManager Object instance
        /// </summary>
        public static TaskManager GetInstance
        {
            get { return instance; }
        }

        /// <summary>
        /// Fires a task added event with the specified task.
        /// </summary>
        /// <param name="task">The Task object</param>
        public static void AddTask(Task task, string taskName = "")
        {
            if (!CurrentTasks.Keys.Contains<Task>(task))
                CurrentTasks.Add(task, taskName);

            GetInstance.OnAddTask(new TaskEventArgs(task, taskName));
        }

        /// <summary>
        /// Fires the add task event to anybody listening to the event.
        /// </summary>
        /// <param name="e">Task event arguments</param>
        protected virtual void OnAddTask(TaskEventArgs e)
        {
            EventHandler<TaskEventArgs> handler = TaskAddedEvent;
            if (handler != null) handler(this, e);
        }

        /// <summary>
        /// Fires a task removed event with the specified task.
        /// </summary>
        /// <param name="task">The Task object</param>
        public static void RemoveTask(Task task)
        {
            if (CurrentTasks.Keys.Contains<Task>(task))
            {
                string taskName = CurrentTasks[task];
                if (CurrentTasks.Remove(task))
                    GetInstance.OnRemoveTask(new TaskEventArgs(task, taskName));
            }
        }

        /// <summary>
        /// Fires the remove task event to anybody listening to the event.
        /// </summary>
        /// <param name="e">Task event arguments</param>
        protected virtual void OnRemoveTask(TaskEventArgs e)
        {
            EventHandler<TaskEventArgs> handler = TaskRemovedEvent;
            if (handler != null) handler(this, e);
        }

        /// <summary>
        /// Creates a Task that will complete only when all of the provided
        /// collection of Tasks has been completed.
        /// </summary>
        /// <param name="tasks">The tasks to monitor for completion</param>
        /// <returns>Task for collection of Tasks</returns>
        public static Task WhenAll(params Task[] tasks)
        {
            return TaskEx.WhenAll(tasks);
        }

        /// <summary>
        /// Creates a Task that will complete only when all of the provided
        /// collection of Tasks has been completed. If Tasks were created through
        /// TaskManager.Run method they don't need to be wrapped again as they
        /// are already wrapped in the event handler mechanism.
        /// </summary>
        /// <param name="wrapTasks">If true tasks will be wrapped in add/remove event handler</param>
        /// <param name="tasks">The tasks to monitor for completion</param>
        /// <returns>Task for collection of Tasks</returns>
        public static Task WhenAll(bool wrapTasks, params Task[] tasks)
        {
            if (wrapTasks) return TaskEx.WhenAll(WrapTasks(tasks));
            else return TaskEx.WhenAll(tasks);
        }

        /// <summary>
        /// Creates a Task that will complete only when all of the provided
        /// collection of Tasks has been completed. If Tasks were created through
        /// TaskManager.Run method they don't need to be wrapped again as they
        /// are already wrapped in the event handler mechanism.
        /// </summary>
        /// <param name="tasks">The tasks to monitor for completion</param>
        /// <param name="wrapTasks">If true tasks will be wrapped in add/remove event handler</param>
        /// <returns>Task for collection of Tasks</returns>
        public static Task WhenAll(IEnumerable<Task> tasks, bool wrapTasks = false)
        {
            if (wrapTasks) return TaskEx.WhenAll(WrapTasks(tasks));
            else return TaskEx.WhenAll(tasks);
        }

        /// <summary>
        /// Creates a Task that runs the specified Action. The Task will be wrapped
        /// in the event handler mechanism so it can be tracked by TaskManager.
        /// </summary>
        /// <param name="action">The action to execute asynchronously</param>
        /// <returns>Task that runs the specified Action</returns>
        public static Task Run(Action action)
        {
            //return TaskEx.Run(() => WrapTask(action));
            return WrapTask(action);
        }

        /// <summary>
        /// Creates a Task that runs the specified Action. The Task will be wrapped
        /// in the event handler mechanism so it can be tracked by TaskManager, and
        /// it can be given a task name for easier identification and visualization.
        /// </summary>
        /// <param name="taskName">Name for the task. Does not need to be unique</param>
        /// <param name="action">The action to execute asynchronously</param>
        /// <returns>Task that runs the specified Action</returns>
        public static Task Run(string taskName, Action action)
        {
            //return TaskEx.Run(() => WrapTask(action, taskName));
            return WrapTask(action, taskName);
        }

        public static Task<TResult> Run<TResult>(string taskName, Func<TResult> action)
        {
            //return TaskEx.Run<Task<TResult>>(() => WrapTask<TResult>(action, taskName)).Result;
            return WrapTask<TResult>(action, taskName);
        }

        /// <summary>
        /// Creates a Task that runs the specified Task after wrapping it
        /// in the event handler mechanism so it can be tracked by TaskManager.
        /// </summary>
        /// <param name="task">The task to execute asynchronously</param>
        /// <returns>Task that runs the specified Action</returns>
        private static Task WrapTask(Task task)
        {
            return WrapTask(task, string.Empty);
        }

        /// <summary>
        /// Creates a Task that runs the specified Task after wrapping it
        /// in the event handler mechanism so it can be tracked by TaskManager, and
        /// it can be given a task name for easier identification and visualization.
        /// </summary>
        /// <param name="task">The task to execute asynchronously</param>
        /// <param name="taskName">Name for the task. Does not need to be unique</param>
        /// <returns>Task that runs the specified Action</returns>
        private static Task WrapTask(Task task, string taskName)
        {
            return WrapTask(task, taskName, CancellationToken.None);
        }

        /// <summary>
        /// Creates a Task that runs the specified Action after wrapping it
        /// in the event handler mechanism so it can be tracked by TaskManager.
        /// </summary>
        /// <param name="action">The action to execute asynchronously</param>
        /// <returns>Task that runs the specified Action</returns>
        private static Task WrapTask(Action action)
        {
            return WrapTask(action, string.Empty);
        }

        /// <summary>
        /// Creates a Task that runs the specified Action after wrapping it
        /// in the event handler mechanism so it can be tracked by TaskManager, and
        /// it can be given a task name for easier identification and visualization.
        /// </summary>
        /// <param name="action">The action to execute asynchronously</param>
        /// <param name="taskName">Name for the task. Does not need to be unique</param>
        /// <returns>Task that runs the specified Action</returns>
        private static Task WrapTask(Action action, string taskName)
        {
            return WrapTask(action, taskName, CancellationToken.None);
        }

        /// <summary>
        /// Creates a Task that runs the specified Function after wrapping it
        /// in the event handler mechanism so it can be tracked by TaskManager, and
        /// it can be given a task name for easier identification and visualization.
        /// </summary>
        /// <typeparam name="TResult">The type of the return value of the function</typeparam>
        /// <param name="action">The action to execute asynchronously</param>
        /// <param name="taskName">Name for the task. Does not need to be unique</param>
        /// <returns>Task that runs the specified Function</returns>
        private static Task<TResult> WrapTask<TResult>(Func<TResult> action, string taskName)
        {
            return WrapTask<TResult>(action, taskName, CancellationToken.None);
        }

        /// <summary>
        /// Creates a Task that runs the specified Action after wrapping it
        /// in the event handler mechanism so it can be tracked by TaskManager.
        /// The task can take a CancellationToken to signal the cancel flag to the task.
        /// </summary>
        /// <param name="action">The action to execute asynchronously</param>
        /// <param name="cancellationToken">The CancellationToken for the wrapped Task</param>
        /// <returns>Task that runs the specified Action</returns>
        private static Task WrapTask(Action action, CancellationToken cancellationToken)
        {
            return WrapTask(TaskEx.Run(action), string.Empty);
        }

        /// <summary>
        /// Creates a Task that runs the specified Action after wrapping it
        /// in the event handler mechanism so it can be tracked by TaskManager, and
        /// it can be given a task name for easier identification and visualization.
        /// The task can take a CancellationToken to signal the cancel flag to the task.
        /// </summary>
        /// <param name="action">The action to execute asynchronously</param>
        /// <param name="taskName">Name for the task. Does not need to be unique</param>
        /// <param name="cancellationToken">The CancellationToken for the wrapped Task</param>
        /// <returns>Task that runs the specified Action</returns>
        private static Task WrapTask(Action action, string taskName, CancellationToken cancellationToken)
        {
            return WrapTask(TaskEx.Run(action), taskName, cancellationToken);
        }

        /// <summary>
        /// Creates a Task that runs the specified Function after wrapping it
        /// in the event handler mechanism so it can be tracked by TaskManager, and
        /// it can be given a task name for easier identification and visualization.
        /// The task can take a CancellationToken to signal the cancel flag to the task.
        /// </summary>
        /// <typeparam name="TResult">The type of the return value of the function</typeparam>
        /// <param name="action">The action to execute asynchronously</param>
        /// <param name="taskName">Name for the task. Does not need to be unique</param>
        /// <param name="cancellationToken">The CancellationToken for the wrapped Task</param>
        /// <returns>Task that runs the specified Function</returns>
        private static Task<TResult> WrapTask<TResult>(Func<TResult> action, string taskName, CancellationToken cancellationToken)
        {
            return WrapTask<TResult>(TaskEx.Run<TResult>(action), taskName, cancellationToken);
        }

        /// <summary>
        /// Creates a Task that runs the specified Task after wrapping it
        /// in the event handler mechanism so it can be tracked by TaskManager.
        /// The task can take a CancellationToken to signal the cancel flag to the task.
        /// </summary>
        /// <param name="task">The task to execute asynchronously</param>
        /// <param name="cancellationToken">The CancellationToken for the wrapped Task</param>
        /// <returns>Task that runs the specified Action</returns>
        private static Task WrapTask(Task task, CancellationToken cancellationToken)
        {
            return WrapTask(task, string.Empty, cancellationToken);
        }

        /// <summary>
        /// Creates a Task that runs the specified Task after wrapping it
        /// in the event handler mechanism so it can be tracked by TaskManager, and
        /// it can be given a task name for easier identification and visualization.
        /// The task can take a CancellationToken to signal the cancel flag to the task.
        /// </summary>
        /// <param name="task">The task to execute asynchronously</param>
        /// <param name="taskName">Name for the task. Does not need to be unique</param>
        /// <param name="cancellationToken">The CancellationToken for the wrapped Task</param>
        /// <returns>Task that runs the specified Action</returns>
        private static Task WrapTask(Task task, string taskName, CancellationToken cancellationToken)
        {
            return TaskEx.Run(() =>
                {
                    AddTask(task, taskName);
                    task.Wait();
                    RemoveTask(task);
                }, cancellationToken);
        }

        /// <summary>
        /// Creates a Task that runs the specified Function Task after wrapping it
        /// in the event handler mechanism so it can be tracked by TaskManager, and
        /// it can be given a task name for easier identification and visualization.
        /// The task can take a CancellationToken to signal the cancel flag to the task.
        /// </summary>
        /// <typeparam name="TResult">The type of the return value of the task</typeparam>
        /// <param name="task">The task to execute asynchronously</param>
        /// <param name="taskName">Name for the task. Does not need to be unique</param>
        /// <param name="cancellationToken">The CancellationToken for the wrapped Task</param>
        /// <returns>Task that runs the specified Task</returns>
        private static Task<TResult> WrapTask<TResult>(Task<TResult> task, string taskName, CancellationToken cancellationToken)
        {
            return TaskEx.Run<TResult>(() =>
                {
                    AddTask(task, taskName);
                    task.Wait();
                    RemoveTask(task);
                    return task.Result;
                }, cancellationToken);
        }

        /// <summary>
        /// Creates a list of Tasks that run the specified set of Tasks after wrapping them
        /// in the event handler mechanism so they can be tracked by TaskManager. Typically
        /// used by the TaskManager.WhenAll and WhenAny methods.
        /// </summary>
        /// <param name="tasks"></param>
        /// <returns></returns>
        private static List<Task> WrapTasks(IEnumerable<Task> tasks)
        {
            List<Task> wrappedTasks = new List<Task>();
            foreach (Task task in tasks)
                wrappedTasks.Add(WrapTask(task));

            return wrappedTasks;
        }
    }

    /// <summary>
    /// Used to separate tasks into different categories.
    /// </summary>
    public enum TaskType { Scraper, Crawler }

    /// <summary>
    /// Log message event argument object which is used when firing the LogMessage event.
    /// </summary>
    public class TaskEventArgs : EventArgs
    {
        /// <summary>
        /// The Task object
        /// </summary>
        public Task Task { get; set; }
        /// <summary>
        /// The type of the Task
        /// </summary>
        public TaskType Type { get; set; }
        /// <summary>
        /// The name of the Task
        /// </summary>
        public string TaskName { get; set; }

        /// <summary>
        /// Constructor for task event arguments object. Takes Task and optional Task Name
        /// as input parameter.
        /// </summary>
        /// <param name="task">Task</param>
        /// <param name="taskName">Task Name</param>
        public TaskEventArgs(Task task, string taskName = "")
        {
            Task = task;
            TaskName = taskName;
        }

        /// <summary>
        /// Constructor for task event arguments object. Takes Task, Type, and optional
        /// Task Name as input parameter.
        /// </summary>
        /// <param name="task">Task</param>
        /// <param name="type">Task Type</param>
        /// <param name="taskName">Task Name</param>
        public TaskEventArgs(Task task, TaskType type, string taskName = "")
            : this(task, taskName)
        {
            Type = type;
        }
    }
}
